/*
 * Copyright (C) 2012-2013 Michael L.R. Marques
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 * 
 * Contact: michaellrmarques@gmail.com
 */

package com.jm.commons.settings;

import com.jm.commons.fio.FileSystem;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 * Settings API to simplify saving and loading dynamically
 * 
 * Use as a parent class and do the value casting in methods
 * code{
 *  public class Settings extends com.jm.commons.settings.Settings {
 *  
 *   public static void setNumber(int number) {
 *       setValue("number", String.valueOf(number));
 *   }
 * 
 *   public static int getNumber() {
 *       return Integer.parseInt(getValue("number", number));
 *   }
 * 
 *  }
 * 
 * }
 * 
 * @author Michael L.R. Marques
 */
public class Settings {
    
    /**
     * The system file that will be used to load and save the Xml Document
     */
    private static File settingsFile = new File("./settings/settings.xml");
    
    /**
     * The Xml Document that will be used by the Settings API
     */
    private static Document xmlDocument;

    /**
     * Loads the Xml Document file into memory
     * 
     * @throws com.jm.commons.settings.SettingsException
     */
    public static void load() throws SettingsException {
        // Check if the directory exists, if not, create it
        if (!settingsFile.getParentFile().exists()) {
            settingsFile.getParentFile().mkdirs();
        }
        // Check if the settings file exists, if not, create it
        if (settingsFile.exists()) {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            factory.setNamespaceAware(true);
            try {
                xmlDocument = factory.newDocumentBuilder().parse(settingsFile);
            } catch (ParserConfigurationException | SAXException | IOException e) {
                throw new SettingsException(e);
            }
        } else {
            try {
                DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
                DocumentBuilder builder = factory.newDocumentBuilder();
                xmlDocument = builder.parse(new InputSource(new StringReader("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<settings />")));
            } catch (SAXException | IOException | ParserConfigurationException e) {
                throw new SettingsException(e);
            }
        }
    }

    /**
     * Saves the changed Xml Document
     * 
     * @throws com.jm.commons.settings.SettingsException
     */
    public static void save() throws SettingsException {
        try {
            TransformerFactory.newInstance().newTransformer().transform(new DOMSource(xmlDocument), new StreamResult(settingsFile));
        } catch (TransformerException te) {
            throw new SettingsException(te);
        }
    }
    
    /**
     * Sets the value of an attribute in a settings node
     * 
     * @param name
     * @param value 
     * @throws com.jm.commons.settings.SettingsException 
     */
    protected static void setValue(String name, String value) throws SettingsException {
        try {
            if (XPathFactory.newInstance().newXPath().evaluate("/settings/" + name, xmlDocument, XPathConstants.NODE) == null) {
                Node node = ((Node) XPathFactory.newInstance().newXPath().evaluate("/settings", xmlDocument, XPathConstants.NODE)).appendChild(xmlDocument.createElement(name));
                NamedNodeMap atts = node.getAttributes();
                Attr attr = xmlDocument.createAttribute("value");
                attr.setNodeValue(value);
                atts.setNamedItem(attr);
            } else if (XPathFactory.newInstance().newXPath().evaluate("/settings/" + name + "/@value", xmlDocument, XPathConstants.NODE) == null) {
                Node node = (Node) XPathFactory.newInstance().newXPath().evaluate("/settings/" + name, xmlDocument, XPathConstants.NODE);
                NamedNodeMap atts = node.getAttributes();
                Attr attr = xmlDocument.createAttribute("value");
                attr.setNodeValue(value);
                atts.setNamedItem(attr);
            } else {
                ((Node) XPathFactory.newInstance().newXPath().evaluate("/settings/" + name + "/@value", xmlDocument, XPathConstants.NODE)).setNodeValue(value);
            }
        } catch (XPathExpressionException xpee) {
            throw new SettingsException(xpee);
        }
    }

    /**
     * Gets the value of an attribute in a settings node
     * 
     * @param name
     * @return 
     * @throws com.jm.commons.settings.SettingsException 
     */
    protected static String getValue(String name) throws SettingsException {
        try {
            return XPathFactory.newInstance().newXPath().evaluate("/settings/" + name + "/@value", xmlDocument, XPathConstants.STRING).toString();
        } catch (XPathExpressionException | NullPointerException e) {
            throw new SettingsException(e);
        }
    }
    
    /**
     * Sets the default of an attribute in a settings node
     * 
     * @param name
     * @param value 
     * @throws com.jm.commons.settings.SettingsException 
     */
    protected static void setDefault(String name, String value) throws SettingsException {
        try {
            if (XPathFactory.newInstance().newXPath().evaluate("/settings/" + name, xmlDocument, XPathConstants.NODE) == null) {
                Node node = ((Node) XPathFactory.newInstance().newXPath().evaluate("/settings", xmlDocument, XPathConstants.NODE)).appendChild(xmlDocument.createElement(name));
                NamedNodeMap atts = node.getAttributes();
                Attr attr = xmlDocument.createAttribute("default");
                attr.setNodeValue(value);
                atts.setNamedItem(attr);
            } else if (XPathFactory.newInstance().newXPath().evaluate("/settings/" + name + "/@default", xmlDocument, XPathConstants.NODE) == null) {
                Node node = (Node) XPathFactory.newInstance().newXPath().evaluate("/settings/" + name, xmlDocument, XPathConstants.NODE);
                NamedNodeMap atts = node.getAttributes();
                Attr attr = xmlDocument.createAttribute("default");
                attr.setNodeValue(value);
                atts.setNamedItem(attr);
            } else {
                ((Node) XPathFactory.newInstance().newXPath().evaluate("/settings/" + name + "/@default", xmlDocument, XPathConstants.NODE)).setNodeValue(value);
            }
        } catch (XPathExpressionException xpee) {
            throw new SettingsException(xpee);
        }        
    }
    
    /**
     * Gets the default of an attribute in a settings node
     * 
     * @param name
     * @return 
     * @throws com.jm.commons.settings.SettingsException 
     */
    protected static String getDefault(String name) throws SettingsException {
        try {
            return XPathFactory.newInstance().newXPath().evaluate("/settings/" + name + "/@default", xmlDocument, XPathConstants.STRING).toString();
        } catch (XPathExpressionException | NullPointerException e) {
            throw new SettingsException(e);
        }
    }
    
    /**
     * Set the system settings file
     * 
     * @param file 
     * @throws java.io.IOException 
     */
    public static void setSettingsFile(File file) throws IOException {
        // Make sure the file is an Xml file
        if (FileSystem.isValidExtension(file, new String[] { "xml" })) {
            throw new IOException("The settings file must be of type xml");
        }
        settingsFile = file;
    }
    
    /**
     * Get the system settings file
     * 
     * @return 
     */
    public static File getSettingsFile() {
        return settingsFile;
    }
    
}
